package org.rainwalk.library.service.impl;

import cn.hutool.core.codec.Base64;
import cn.hutool.core.io.FileUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSONObject;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.elasticsearch.action.delete.DeleteRequest;
import org.elasticsearch.action.get.GetRequest;
import org.elasticsearch.action.index.IndexRequest;
import org.elasticsearch.action.search.SearchRequest;
import org.elasticsearch.action.search.SearchResponse;
import org.elasticsearch.client.RequestOptions;
import org.elasticsearch.client.RestHighLevelClient;
import org.elasticsearch.common.xcontent.XContentType;
import org.elasticsearch.index.query.BoolQueryBuilder;
import org.elasticsearch.index.query.MatchQueryBuilder;
import org.elasticsearch.index.query.QueryBuilders;
import org.elasticsearch.index.query.TermQueryBuilder;
import org.elasticsearch.search.SearchHit;
import org.elasticsearch.search.SearchHits;
import org.elasticsearch.search.builder.SearchSourceBuilder;
import org.elasticsearch.search.fetch.subphase.highlight.HighlightBuilder;
import org.elasticsearch.search.fetch.subphase.highlight.HighlightField;
import org.rainwalk.library.entity.Storage;
import org.rainwalk.library.mapper.StorageMapper;
import org.rainwalk.library.model.domain.SubFile;
import org.rainwalk.library.model.enums.FileType;
import org.rainwalk.library.model.vo.HighlightVO;
import org.rainwalk.library.model.vo.LibrarySearchVO;
import org.rainwalk.library.service.IFileService;
import org.rainwalk.library.service.ILibraryService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;

import java.io.File;
import java.io.IOException;
import java.util.*;
import java.util.stream.Collectors;

/**
 * es library索引服务实现
 *
 * @author 趁雨行 2021/7/8
 */
@Slf4j
@Service
@RequiredArgsConstructor(onConstructor_={@Autowired})
public class LibraryServiceImpl implements ILibraryService {

    private final StorageMapper storageMapper;

    private final RestHighLevelClient restClient;

    private final List<IFileService> fileServices;

    private static final String INDEX = "library";

    @Override
    public void deleteById(Storage storage) {
        Long id = storage.getId();
        log.info("ES删除library-----服务层-----删除文档，入参：{}", storage);
        Optional<IFileService> fileServiceOptional = getFileService(FileType.getByDesc(storage.getFileType()));
        if (!fileServiceOptional.isPresent()) {
            log.error("ES删除library-----服务层-----删除失败，找不到支持改文件类型的服务：{}", storage);
            return;
        }
        IFileService fileService = fileServiceOptional.get();
        SearchResponse response;
        try {
            SearchRequest request = new SearchRequest(INDEX);
            TermQueryBuilder termQueryBuilder = new TermQueryBuilder("fileId", id.toString());
            SearchSourceBuilder builder = new SearchSourceBuilder();
            builder.query(termQueryBuilder);
            request.source(builder);
            response = restClient.search(request, RequestOptions.DEFAULT);
        } catch (IOException e) {
            log.info("ES删除library-----服务层-----查询文档失败：id={}", id);
            return;
        }
        if (response.getHits().getHits().length > 0) {
            for (SearchHit hit : response.getHits()) {
                String esId = hit.getId();
                log.info("ES删除library-----服务层-----查询到文档，准备删除：id={}", esId);
                DeleteRequest deleteRequest = new DeleteRequest(INDEX, esId);
                try {
                    restClient.delete(deleteRequest, RequestOptions.DEFAULT);
                } catch (IOException e) {
                    log.error("ES删除library-----服务层-----删除文档失败：id={}", esId);
                }
                log.info("ES删除library-----服务层-----删除文档成功：id={}", esId);
            }
        }
        //清除文件
        Optional<File> fileOptional = fileService.fetchFile(storage.getRelativePath());
        fileOptional.ifPresent(file -> {
            log.info("ES删除library-----服务层-----清除本地文件：{}", storage);
            FileUtil.del(file);
        });
        //更新mysql写入记录
        log.info("ES删除library-----服务层-----更新mysql写入标识：{}", id);
        storageMapper.undoEsById(id);
        log.info("ES删除library-----服务层-----更新mysql写入标识成功：{}", id);
    }

    @Override
    public List<LibrarySearchVO> search(String keyword) throws IOException {
        SearchSourceBuilder source = new SearchSourceBuilder();
        //返回字段控制
        String[] includeFields = new String[] {"fileId", "subNo"};
        source.fetchSource(includeFields, null);

        //查询条件
        matchContent(keyword, source);

        //设置高亮显示
        setHighlight(source);

        SearchRequest request = new SearchRequest(INDEX);
        request.source(source);
        SearchResponse search = restClient.search(request, RequestOptions.DEFAULT);
        SearchHits hits = search.getHits();
        List<LibrarySearchVO> librarySearchVOS = hits2LibrarySearchVO(hits);

        return librarySearchVOS;
    }

    private void matchContent(String keyword, SearchSourceBuilder source) {
        MatchQueryBuilder matchQueryBuilder = QueryBuilders.matchQuery("attachment.content", keyword);
        BoolQueryBuilder boolBuilder = QueryBuilders.boolQuery();
        boolBuilder.must(matchQueryBuilder);
        source.query(matchQueryBuilder);
    }

    private List<LibrarySearchVO> hits2LibrarySearchVO(SearchHits hits) {
        Map<String, LibrarySearchVO> fileMap = new HashMap<>();
        for (SearchHit hit : hits) {
            Map<String, Object> sourceMap = hit.getSourceAsMap();
            String fileId = (String) sourceMap.get("fileId");
            String subNo = (String) sourceMap.get("subNo");
            LibrarySearchVO librarySearchVO = fileMap.computeIfAbsent(fileId, k -> new LibrarySearchVO());
            //拼接返回数据
            librarySearchVO.setId(new Long(fileId));
            Map<String, HighlightField> highlightFields = hit.getHighlightFields();
            HighlightField highlightField = highlightFields.get("attachment.content");
            if (highlightField != null) {
                List<HighlightVO> highlights = Arrays.stream(highlightField.getFragments())
                        .map(text -> {
                            String highlight = text.string().replaceAll("[\t\n\r�]", "");
                            HighlightVO highlightVO = new HighlightVO();
                            highlightVO.setHighlight(highlight);
                            highlightVO.setSubNo(subNo);
                            return highlightVO;
                        }).collect(Collectors.toList());
                List<HighlightVO> voHighlights = librarySearchVO.getHighlights();
                if (voHighlights == null) {
                    librarySearchVO.setHighlights(highlights);
                } else {
                    voHighlights.addAll(highlights);
                }
            }
        }
        return new ArrayList<>(fileMap.values());
    }

    @Override
    public Page<LibrarySearchVO> searchPage(String keyWord, Integer pageNum, Integer pageSize) throws IOException {
        SearchSourceBuilder source = new SearchSourceBuilder();
        //分页
        int from = pageNum < 1 ? 0 : pageNum - 1;
        source.from(from);
        source.size(pageSize);
        //返回字段控制
        String[] includeFields = new String[] {"fileId", "subNo"};
        source.fetchSource(includeFields, null);

        //查询条件
        matchContent(keyWord, source);

        //设置高亮显示
        setHighlight(source);

        SearchRequest request = new SearchRequest(INDEX);
        request.source(source);
        SearchResponse search = restClient.search(request, RequestOptions.DEFAULT);
        SearchHits hits = search.getHits();
        List<LibrarySearchVO> librarySearchVOS = hits2LibrarySearchVO(hits);

        Page<LibrarySearchVO> page = new Page<>();
        page.setSize(pageSize);
        page.setCurrent(from + 1L);
        page.setRecords(librarySearchVOS);
        page.setTotal(hits.getTotalHits().value);

        return page;
    }

    private void setHighlight(SearchSourceBuilder source) {
        HighlightBuilder highlightBuilder = new HighlightBuilder().field("*").requireFieldMatch(false);
        highlightBuilder.preTags("<span style=\"color:red\">");
        highlightBuilder.postTags("</span>");

        source.highlighter(highlightBuilder);
    }

    @Override
    @Async
    public void write(Storage storage) {
        log.info("ES写入library-----服务层-----异步es写入，入参：{}", storage);
        Optional<IFileService> fileServiceOptional = getFileService(FileType.getByDesc(storage.getFileType()));
        if (!fileServiceOptional.isPresent()) {
            log.error("ES写入library-----服务层-----找不到支持改文件类型的服务：{}", storage);
            return;
        }
        IFileService fileService = fileServiceOptional.get();
        Optional<File> fileOptional = fileService.fetchFile(storage.getRelativePath());
        if (! fileOptional.isPresent()) {
            log.warn("ES写入library-----服务层-----本地文件不存在：{}", storage);
            return;
        }
        //文件拆分写入
        File file = fileOptional.get();
        List<SubFile> subFiles;
        try {
            subFiles = fileService.splitFile(file);
        } catch (IOException e) {
            log.warn("ES写入library-----服务层-----文件拆分失败：{}", storage, e);
            return;
        }
        for (SubFile slicing : subFiles) {
            String id = storage.getId() + "_" + slicing.getSubNo();
            GetRequest getRequest = new GetRequest(INDEX, id);
            boolean exists;
            try {
                exists = restClient.exists(getRequest, RequestOptions.DEFAULT);
            } catch (IOException e) {
                log.info("ES写入library-----服务层-----查询文档失败：id={}", storage.getId(), e);
                fileService.cleanResourceOnSplitFile(file, subFiles);
                return;
            }
            if (!exists) {
                log.debug("ES写入library-----服务层-----es写入文件：id={}", storage.getId());
                String content = Base64.encode(slicing.getFile());
                JSONObject json = new JSONObject();
                json.set("fileId", storage.getId().toString());
                json.set("subNo", slicing.getSubNo());
                json.set("content", StrUtil.removeAllLineBreaks(content));
                IndexRequest indexRequest = new IndexRequest(INDEX);
                indexRequest.source(json.toString(), XContentType.JSON);
                indexRequest.id(id)
                        .setPipeline("attachment");
                try {
                    restClient.index(indexRequest, RequestOptions.DEFAULT);
                } catch (IOException e) {
                    log.error("ES写入library-----服务层-----es写入子文件失败：id={}, subFile={}", storage.getId(), slicing, e);
                    continue;
                }
                log.debug("ES写入library-----服务层-----es写入文件成功：id={}, subFile={}", storage.getId(), slicing);
            } else {
                log.debug("ES写入library-----服务层-----es已写入, id={}, subFile={}", storage.getId(), slicing);
            }
        }
//        fileService.cleanResourceOnSplitFile(file, subFiles);
        //更新mysql写入记录
        Storage updateStorage = new Storage();
        updateStorage.setId(storage.getId());
        updateStorage.setEsWrite(true);
        log.info("ES写入library-----服务层-----更新mysql写入标识：{}", updateStorage);
        storageMapper.updateById(updateStorage);
        log.info("ES写入library-----服务层-----更新mysql写入标识成功：{}", updateStorage);
        log.info("ES写入library-----服务层-----异步es写入成功：{}", storage);
    }

    /**
     * 根据文件类型获取文件服务Bean
     *
     * @param fileType 文件类型
     * @return
     */
    private Optional<IFileService> getFileService(FileType fileType) {
        Optional<IFileService> first = fileServices.stream()
                .filter(fileService -> fileService.support(fileType).isBool())
                .findFirst();
        return first;
    }
}
